/** @file

  Copyright (c) 2015, ARM Ltd. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "FdtPlatform.h"

STATIC CONST SHELL_PARAM_ITEM ParamList[] = {
  {L"-i", TypeFlag },
  {NULL , TypeMax  }
};

/**
  Display FDT device paths.

  Display in text form the device paths used to install the FDT from the
  highest to the lowest priority.

**/
STATIC
VOID
DisplayFdtDevicePaths (
  VOID
  )
{
  EFI_STATUS  Status;
  UINTN       DataSize;
  CHAR16      *TextDevicePath;
  CHAR16      *TextDevicePaths;
  CHAR16      *TextDevicePathSeparator;

  ShellPrintHiiEx (
    -1, -1, NULL,
    STRING_TOKEN (STR_SETFDT_DEVICE_PATH_LIST),
    mFdtPlatformDxeHiiHandle
    );

  if (FeaturePcdGet (PcdOverridePlatformFdt)) {
    DataSize = 0;
    Status = gRT->GetVariable (
                    L"Fdt",
                    &gFdtVariableGuid,
                    NULL,
                    &DataSize,
                    NULL
                    );

    //
    // Keep going only if the "Fdt" variable is defined.
    //

    if (Status == EFI_BUFFER_TOO_SMALL) {
      TextDevicePath = AllocatePool (DataSize);
      if (TextDevicePath == NULL) {
        return;
      }

      Status = gRT->GetVariable (
                      L"Fdt",
                      &gFdtVariableGuid,
                      NULL,
                      &DataSize,
                      TextDevicePath
                      );
      if (!EFI_ERROR (Status)) {
        ShellPrintHiiEx (
          -1, -1, NULL,
          STRING_TOKEN (STR_SETFDT_DEVICE_PATH),
          mFdtPlatformDxeHiiHandle,
          TextDevicePath
          );
      }

      FreePool (TextDevicePath);
    }
  }

  //
  // Loop over the device path list provided by "PcdFdtDevicePaths". The device
  // paths are in text form and separated by a semi-colon.
  //

  TextDevicePaths = AllocateCopyPool (
                      StrSize ((CHAR16*)PcdGetPtr (PcdFdtDevicePaths)),
                      (CHAR16*)PcdGetPtr (PcdFdtDevicePaths)
                      );
  if (TextDevicePaths == NULL) {
    return;
  }

  for (TextDevicePath = TextDevicePaths;
       *TextDevicePath != L'\0'        ; ) {
    TextDevicePathSeparator = StrStr (TextDevicePath, L";");

    if (TextDevicePathSeparator != NULL) {
      *TextDevicePathSeparator = L'\0';
    }

    ShellPrintHiiEx (
      -1, -1, NULL,
      STRING_TOKEN (STR_SETFDT_DEVICE_PATH),
      mFdtPlatformDxeHiiHandle,
      TextDevicePath
      );

    if (TextDevicePathSeparator == NULL) {
      break;
    }
    TextDevicePath = TextDevicePathSeparator + 1;
  }

  FreePool (TextDevicePaths);
}

/**
  Update the text device path stored in the "Fdt" UEFI variable given
  an EFI Shell file path or a text device path.

  This function is a subroutine of the ShellDynCmdSetFdtHandler() function
  to make its code easier to read.

  @param[in]  Shell          The instance of the shell protocol used in the
                             context of processing the "setfdt" command.
  @param[in]  FilePath       EFI Shell path or the device path to the FDT file.

  @return  SHELL_SUCCESS            The text device path was succesfully updated.
  @return  SHELL_INVALID_PARAMETER  The Shell file path is not valid.
  @return  SHELL_OUT_OF_RESOURCES   A memory allocation failed.
  @return  SHELL_DEVICE_ERROR       The "Fdt" variable could not be saved due to a hardware failure.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable is read-only.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable cannot be deleted.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable could not be written due to security violation.
  @return  SHELL_NOT_FOUND          Device path to text protocol not found.
  @return  SHELL_ABORTED            Operation aborted.

**/
STATIC
SHELL_STATUS
UpdateFdtTextDevicePath (
  IN EFI_SHELL_PROTOCOL  *Shell,
  IN CONST CHAR16        *FilePath
  )
{
  EFI_STATUS                          Status;
  EFI_DEVICE_PATH                     *DevicePath;
  EFI_DEVICE_PATH_TO_TEXT_PROTOCOL    *EfiDevicePathToTextProtocol;
  CHAR16                              *TextDevicePath;
  CHAR16                              *FdtVariableValue;
  EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL  *EfiDevicePathFromTextProtocol;
  SHELL_STATUS                        ShellStatus;

  ASSERT (FilePath != NULL);
  DevicePath       = NULL;
  TextDevicePath   = NULL;
  FdtVariableValue = NULL;

  if (*FilePath != L'\0') {
    DevicePath = Shell->GetDevicePathFromFilePath (FilePath);
    if (DevicePath != NULL) {
      Status = gBS->LocateProtocol (
                      &gEfiDevicePathToTextProtocolGuid,
                      NULL,
                      (VOID **)&EfiDevicePathToTextProtocol
                      );
      if (EFI_ERROR (Status)) {
        goto Error;
      }

      TextDevicePath = EfiDevicePathToTextProtocol->ConvertDevicePathToText (
                                                      DevicePath,
                                                      FALSE,
                                                      FALSE
                                                      );
      if (TextDevicePath == NULL) {
        Status = EFI_OUT_OF_RESOURCES;
        goto Error;
      }
      FdtVariableValue = TextDevicePath;
    } else {
      //
      // Try to convert back the EFI Device Path String into a EFI device Path
      // to ensure the format is valid
      //
      Status = gBS->LocateProtocol (
                      &gEfiDevicePathFromTextProtocolGuid,
                      NULL,
                      (VOID **)&EfiDevicePathFromTextProtocol
                      );
      if (EFI_ERROR (Status)) {
        goto Error;
      }

      DevicePath = EfiDevicePathFromTextProtocol->ConvertTextToDevicePath (
                                                    FilePath
                                                    );
      if (DevicePath == NULL) {
        Status = EFI_INVALID_PARAMETER;
        goto Error;
      }
      FdtVariableValue = (CHAR16*)FilePath;
    }
  }

  Status = gRT->SetVariable (
                  (CHAR16*)L"Fdt",
                  &gFdtVariableGuid,
                  EFI_VARIABLE_RUNTIME_ACCESS    |
                  EFI_VARIABLE_NON_VOLATILE      |
                  EFI_VARIABLE_BOOTSERVICE_ACCESS ,
                  (FdtVariableValue != NULL) ?
                  StrSize (FdtVariableValue) : 0,
                  FdtVariableValue
                  );

Error:
  ShellStatus = EfiCodeToShellCode (Status);
  if (!EFI_ERROR (Status)) {
    if (FdtVariableValue != NULL) {
      ShellPrintHiiEx (
        -1, -1, NULL,
        STRING_TOKEN (STR_SETFDT_UPDATE_SUCCEEDED),
        mFdtPlatformDxeHiiHandle,
        FdtVariableValue
        );
    } else {
      ShellPrintHiiEx (
        -1, -1, NULL,
        STRING_TOKEN (STR_SETFDT_UPDATE_DELETED),
        mFdtPlatformDxeHiiHandle
        );
    }
  } else {
    if (Status == EFI_INVALID_PARAMETER) {
      ShellPrintHiiEx (
        -1, -1, NULL,
        STRING_TOKEN (STR_SETFDT_INVALID_PATH),
        mFdtPlatformDxeHiiHandle,
        FilePath
        );
    } else {
      ShellPrintHiiEx (
        -1, -1, NULL,
        STRING_TOKEN (STR_SETFDT_ERROR),
        mFdtPlatformDxeHiiHandle,
        Status
        );
    }
  }

  if (DevicePath != NULL) {
    FreePool (DevicePath);
  }
  if (TextDevicePath != NULL) {
    FreePool (TextDevicePath);
  }

  return ShellStatus;
}

/**
  This is the shell command "setfdt" handler function. This function handles
  the command when it is invoked in the shell.

  @param[in]  This             The instance of the
                               EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL.
  @param[in]  SystemTable      The pointer to the UEFI system table.
  @param[in]  ShellParameters  The parameters associated with the command.
  @param[in]  Shell            The instance of the shell protocol used in the
                               context of processing this command.

  @return  SHELL_SUCCESS            The operation was successful.
  @return  SHELL_ABORTED            Operation aborted due to internal error.
  @return  SHELL_INVALID_PARAMETER  The parameters of the command are not valid.
  @return  SHELL_INVALID_PARAMETER  The EFI Shell file path is not valid.
  @return  SHELL_NOT_FOUND          Failed to locate a protocol or a file.
  @return  SHELL_UNSUPPORTED        Device path not supported.
  @return  SHELL_OUT_OF_RESOURCES   A memory allocation failed.
  @return  SHELL_DEVICE_ERROR       The "Fdt" variable could not be saved due to a hardware failure.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable is read-only.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable cannot be deleted.
  @return  SHELL_ACCESS_DENIED      The "Fdt" variable could not be written due to security violation.

**/
SHELL_STATUS
EFIAPI
ShellDynCmdSetFdtHandler (
  IN EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL  *This,
  IN EFI_SYSTEM_TABLE                    *SystemTable,
  IN EFI_SHELL_PARAMETERS_PROTOCOL       *ShellParameters,
  IN EFI_SHELL_PROTOCOL                  *Shell
  )
{
  SHELL_STATUS  ShellStatus;
  EFI_STATUS    Status;
  LIST_ENTRY    *ParamPackage;
  BOOLEAN       FilePath;
  CONST CHAR16  *ValueStr;
  CHAR16        *TextDevicePath;

  ShellStatus  = SHELL_SUCCESS;
  ParamPackage = NULL;
  FilePath     = FALSE;

  //
  // Install the Shell and Shell Parameters Protocols on the driver
  // image. This is necessary for the initialisation of the Shell
  // Library to succeed in the next step.
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                  &gImageHandle,
                  &gEfiShellProtocolGuid, Shell,
                  &gEfiShellParametersProtocolGuid, ShellParameters,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    return SHELL_ABORTED;
  }

  //
  // Initialise the Shell Library as we are going to use it.
  // Assert that the return code is EFI_SUCCESS as it should.
  // To anticipate any change is the codes returned by
  // ShellInitialize(), leave in case of error.
  //
  Status = ShellInitialize ();
  if (EFI_ERROR (Status)) {
    ASSERT_EFI_ERROR (Status);
    return SHELL_ABORTED;
  }

  Status = ShellCommandLineParse (ParamList, &ParamPackage, NULL, TRUE);
  if (!EFI_ERROR (Status)) {
    switch (ShellCommandLineGetCount (ParamPackage)) {
    case 1:
      //
      // Case "setfdt" or "setfdt -i"
      //
      if (!ShellCommandLineGetFlag (ParamPackage, L"-i")) {
        DisplayFdtDevicePaths ();
      }
      break;

    case 2:
      //
      // Case "setfdt file_path"    or
      //      "setfdt -i file_path" or
      //      "setfdt file_path -i"
      //
      FilePath = TRUE;
      break;

    default:
      Status = EFI_INVALID_PARAMETER;
    }
  }
  if (EFI_ERROR (Status)) {
    ShellStatus = EfiCodeToShellCode (Status);
    ShellPrintHiiEx (
      -1, -1, NULL,
      STRING_TOKEN (STR_SETFDT_ERROR),
      mFdtPlatformDxeHiiHandle,
      Status
      );
    goto Error;
  }

  //
  // Update the preferred device path for the FDT if asked for.
  //
  if (FilePath) {
    ValueStr = ShellCommandLineGetRawValue (ParamPackage, 1);
    ShellPrintHiiEx (
      -1, -1, NULL,
      STRING_TOKEN (STR_SETFDT_UPDATING),
      mFdtPlatformDxeHiiHandle
      );
    ShellStatus = UpdateFdtTextDevicePath (Shell, ValueStr);
    if (ShellStatus != SHELL_SUCCESS) {
      goto Error;
    }
  }

  //
  // Run the FDT installation process if asked for.
  //
  if (ShellCommandLineGetFlag (ParamPackage, L"-i")) {
    ShellPrintHiiEx (
      -1, -1, NULL,
      STRING_TOKEN (STR_SETFDT_INSTALLING),
      mFdtPlatformDxeHiiHandle
      );
    Status = RunFdtInstallation (&TextDevicePath);
    ShellStatus = EfiCodeToShellCode (Status);
    if (!EFI_ERROR (Status)) {
      ShellPrintHiiEx (
        -1, -1, NULL,
        STRING_TOKEN (STR_SETFDT_INSTALL_SUCCEEDED),
        mFdtPlatformDxeHiiHandle,
        TextDevicePath
        );
      FreePool (TextDevicePath);
    } else {
      if (Status == EFI_INVALID_PARAMETER) {
        ShellPrintHiiEx (
          -1, -1, NULL,
          STRING_TOKEN (STR_SETFDT_INVALID_DEVICE_PATH),
          mFdtPlatformDxeHiiHandle
          );
      } else {
        ShellPrintHiiEx (
          -1, -1, NULL,
          STRING_TOKEN (STR_SETFDT_ERROR),
          mFdtPlatformDxeHiiHandle,
          Status
          );
      }
      DisplayFdtDevicePaths ();
    }
  }

Error:
  gBS->UninstallMultipleProtocolInterfaces (
         gImageHandle,
         &gEfiShellProtocolGuid, Shell,
         &gEfiShellParametersProtocolGuid, ShellParameters,
         NULL
         );
  ShellCommandLineFreeVarList (ParamPackage);

  return ShellStatus;
}

/**
  This is the shell command "setfdt" help handler function. This
  function returns the formatted help for the "setfdt" command.
  The format matchs that in Appendix B of the revision 2.1 of the
  UEFI Shell Specification.

  @param[in]  This      The instance of the EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL.
  @param[in]  Language  The pointer to the language string to use.

  @return  CHAR16*  Pool allocated help string, must be freed by caller.
**/
CHAR16*
EFIAPI
ShellDynCmdSetFdtGetHelp (
  IN EFI_SHELL_DYNAMIC_COMMAND_PROTOCOL  *This,
  IN CONST CHAR8                         *Language
  )
{
  //
  // This allocates memory. The caller has to free the allocated memory.
  //
  return HiiGetString (
                mFdtPlatformDxeHiiHandle,
                STRING_TOKEN (STR_GET_HELP_SETFDT),
                Language
                );
}
